Passed
Push — master ( d7da14...d05aeb )
by MusikAnimal
01:20
created

editcounter.js ➔ getYAxisLabels   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 18
rs 9.4285
cc 1
nc 1
nop 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A editcounter.js ➔ ... ➔ Object.map 0 8 1
1
/**
2
 * Namespaces that have been excluded from view via namespace toggle table.
3
 * @type {Array}
4
 */
5
window.excludedNamespaces = [];
6
7
/**
8
 * Chart labels for the month/yearcount charts.
9
 * @type {Object} Keys are the chart IDs, values are arrays of strings.
10
 */
11
window.chartLabels = {};
12
13
/**
14
 * Number of digits of the max month/year total. We want to keep this consistent
15
 * for aesthetic reasons, even if the updated totals are fewer digits in size.
16
 * @type {Object} Keys are the chart IDs, values are integers.
17
 */
18
window.maxDigits = {};
19
20
$(function () {
21
    // Don't do anything if this isn't a Edit Counter page.
22
    if ($("body.ec").length === 0) {
23
        return;
24
    }
25
26
    // Set up charts.
27
    $('.chart-wrapper').each(function () {
28
        var chartType = $(this).data('chart-type');
29
        if ( chartType === undefined ) {
30
            return false;
31
        }
32
        var data = $(this).data('chart-data');
33
        var labels = $(this).data('chart-labels');
34
        var $ctx = $('canvas', $(this));
35
36
        /** global: Chart */
37
        new Chart($ctx, {
0 ignored issues
show
Unused Code Best Practice introduced by
The object created with new Chart($ctx, {Identif...e))))),false,false)))}) is not used but discarded. Consider invoking another function instead of a constructor if you are doing this purely for side effects.
Loading history...
38
            type: chartType,
39
            data: {
40
                labels: labels,
41
                datasets: [ { data: data } ]
42
            }
43
        });
44
45
        return undefined;
46
    });
47
48
    loadLatestGlobal();
49
50
    // Set up namespace toggle chart.
51
    setupToggleTable(window.namespaceTotals, window.namespaceChart, null, toggleNamespace);
52
});
53
54
/**
55
 * Callback for setupToggleTable(). This will show/hide a given namespace from
56
 * all charts, and update totals and percentages.
57
 * @param  {Object} newData New namespaces and totals, as returned by setupToggleTable.
58
 * @param  {String} key     Namespace ID of the toggled namespace.
59
 */
60
function toggleNamespace(newData, key)
61
{
62
    var total = 0, counts = [];
63
    Object.keys(newData).forEach(function (namespace) {
64
        var count = parseInt(newData[namespace], 10);
65
        counts.push(count);
66
        total += count;
67
    });
68
    var namespaceCount = Object.keys(newData).length;
69
70
    $('.namespaces--namespaces').text(
71
        namespaceCount.toLocaleString() + ' ' +
72
        $.i18n('num-namespaces', namespaceCount)
73
    );
74
    $('.namespaces--count').text(total.toLocaleString());
75
76
    // Now that we have the total, loop through once more time to update percentages.
77
    counts.forEach(function (count) {
78
        // Calculate percentage, rounded to tenths.
79
        var percentage = getPercentage(count, total);
80
81
        // Update text with new value and percentage.
82
        $('.namespaces-table .sort-entry--count[data-value='+count+']').text(
83
            count.toLocaleString() + ' (' + percentage + '%)'
84
        );
85
    });
86
87
    // Loop through month and year charts, toggling the dataset for the newly excluded namespace.
88
    ['year', 'month'].forEach(function (id) {
89
        var chartObj = window[id + 'countsChart'],
90
            nsName = window.namespaces[key] || $.i18n('mainspace');
91
92
        // Figure out the index of the namespace we're toggling within this chart object.
93
        var datasetIndex;
94
        chartObj.data.datasets.forEach(function (dataset, i) {
95
            if (dataset.label === nsName) {
96
                datasetIndex = i;
97
            }
98
        });
99
100
        // Fetch the metadata and toggle the hidden property.
101
        var meta = chartObj.getDatasetMeta(datasetIndex);
102
        meta.hidden = meta.hidden === null ? !chartObj.data.datasets[datasetIndex].hidden : null;
103
104
        // Add this namespace to the list of excluded namespaces.
105
        if (meta.hidden) {
106
            window.excludedNamespaces.push(nsName);
107
        } else {
108
            window.excludedNamespaces = window.excludedNamespaces.filter(function (namespace) {
109
                return namespace !== nsName;
110
            });
111
        }
112
113
        // Update y-axis labels with the new totals.
114
        window[id + 'countsChart'].config.data.labels = getYAxisLabels(id, chartObj.data.datasets);
115
116
        // Refresh chart.
117
        chartObj.update();
118
    });
119
}
120
121
/**
122
 * Load recent global edits' HTML via AJAX, to not slow down the initial page load.
123
 * Only load if container is present, which is missing in subroutes, e.g. ec-namespacetotals, etc.
124
 */
125
function loadLatestGlobal()
126
{
127
    var $latestGlobalContainer = $("#latestglobal-container");
128
129
    if ($latestGlobalContainer[0]) {
130
        /** global: xtBaseUrl */
131
        var url = xtBaseUrl + 'ec-latestglobal/'
132
            + $latestGlobalContainer.data('project') + '/'
133
            + $latestGlobalContainer.data('username') + '?htmlonly=yes';
134
        $.ajax({
135
            url: url,
136
            timeout: 30000
137
        }).done(function (data) {
138
            $latestGlobalContainer.replaceWith(data);
139
        }).fail(function (_xhr, _status, message) {
140
            $latestGlobalContainer.replaceWith(
141
                $.i18n('api-error', 'Global contributions API: <code>' + message + '</code>')
142
            );
143
        });
144
    }
145
}
146
147
/**
148
 * Build the labels for the y-axis of the year/monthcount charts,
149
 * which include the year/month and the total number of edits across
150
 * all namespaces in that year/month.
151
 * @param {String} id ID prefix of the chart, either 'month' or 'year'.
152
 * @param {Array} datasets Datasets making up the chart.
153
 * @return {Array} Labels for each year/month.
154
 */
155
function getYAxisLabels(id, datasets)
156
{
157
    var labelsAndTotals = getMonthYearTotals(id, datasets);
158
159
    // Format labels with totals next to them. This is a bit hacky,
160
    // but it works! We use tabs (\t) to make the labels/totals
161
    // for each namespace line up perfectly.
162
    // The caveat is that we can't localize the numbers because
163
    // the commas are not monospaced :(
164
    return Object.keys(labelsAndTotals).map(function (year) {
165
        var digitCount = labelsAndTotals[year].toString().length;
166
        var numTabs = (window.maxDigits[id] - digitCount) * 2;
167
168
        // +5 for a bit of extra spacing.
169
        return year + Array(numTabs + 5).join("\t") +
170
            labelsAndTotals[year];
171
    });
172
}
173
174
/**
175
 * Get the total number of edits for the given dataset (year or month).
176
 * @param {String} id ID prefix of the chart, either 'month' or 'year'.
177
 * @param {Array} datasets Datasets making up the chart.
178
 * @return {Object} Labels for each year/month as keys, totals as the values.
179
 */
180
function getMonthYearTotals(id, datasets)
181
{
182
    var labelsAndTotals = {};
183
    datasets.forEach(function (namespace) {
184
        if (window.excludedNamespaces.indexOf(namespace.label) !== -1) {
185
            return;
186
        }
187
188
        namespace.data.forEach(function (count, index) {
189
            if (!labelsAndTotals[window.chartLabels[id][index]]) {
190
                labelsAndTotals[window.chartLabels[id][index]] = 0;
191
            }
192
            labelsAndTotals[window.chartLabels[id][index]] += count;
193
        });
194
    });
195
196
    return labelsAndTotals;
197
}
198
199
/**
200
 * Calculate and format a percentage, rounded to the tenths place.
201
 * @param  {Number} numerator
202
 * @param  {Number} denominator
203
 * @return {Number}
204
 */
205
function getPercentage(numerator, denominator)
206
{
207
    return +(Math.round(
208
        ((numerator / denominator) * 100) + 'e+1'
209
    ) + 'e-1');
210
}
211
212
/**
213
 * Set up the monthcounts or yearcounts chart. This is set on the window
214
 * because it is called in the yearcounts/monthcounts view.
215
 * @param {String} id 'year' or 'month'.
216
 * @param {Array} datasets Datasets grouped by mainspace.
217
 * @param {Array} labels The bare labels for the y-axis (years or months).
218
 * @param {Number} maxTotal Maximum value of year/month totals.
219
 */
220
window.setupMonthYearChart = function (id, datasets, labels, maxTotal) {
221
    /** @type {Array} Labels for each namespace. */
222
    var namespaces = datasets.map(function (dataset) {
223
        return dataset.label;
224
    });
225
226
    window.maxDigits[id] = maxTotal.toString().length
227
    window.chartLabels[id] = labels;
228
229
    window[id + 'countsChart'] = new Chart($('#' + id + 'counts-canvas'), {
230
        type: 'horizontalBar',
231
        data: {
232
            labels: getYAxisLabels(id, datasets),
233
            datasets: datasets
234
        },
235
        options: {
236
            tooltips: {
237
                mode: 'nearest',
238
                intersect: true,
239
                callbacks: {
240
                    label: function (tooltip) {
241
                        var labelsAndTotals = getMonthYearTotals(id, datasets),
242
                            totals = Object.keys(labelsAndTotals).map(function (label) {
243
                                return labelsAndTotals[label];
244
                            }),
245
                            total = totals[tooltip.index],
246
                            percentage = getPercentage(tooltip.xLabel, total);
247
248
                        return tooltip.xLabel.toLocaleString() + ' ' +
249
                            '(' + percentage + '%)';
250
                    },
251
                    title: function (tooltip) {
252
                        var yLabel = tooltip[0].yLabel.replace(/\t.*/, '');
253
                        return yLabel + ' - ' + namespaces[tooltip[0].datasetIndex];
254
                    }
255
                }
256
            },
257
            responsive: true,
258
            maintainAspectRatio: false,
259
            scales: {
260
                xAxes: [{
261
                    stacked: true,
262
                    ticks: {
263
                        beginAtZero: true,
264
                        callback: function (value) {
265
                            if (Math.floor(value) === value) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if Math.floor(value) === value is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
266
                                return value.toLocaleString();
267
                            }
268
                        }
269
                    }
270
                }],
271
                yAxes: [{
272
                    stacked: true,
273
                    barThickness: 18,
274
                }]
275
            },
276
            legend: {
277
                display: false
278
            }
279
        }
280
    });
281
}
282